Keep one bad custom list from blowing up the entries sweep (PP-4787)#3550
Keep one bad custom list from blowing up the entries sweep (PP-4787)#3550dbernstein wants to merge 1 commit into
Conversation
A custom list whose auto_update_query contained an invalid `published` value (e.g. `2025>01>01`) failed its per-list task and, because that task is a chord header, stopped the chord body from ever running. The body is what releases the sweep-level Redis lock, so the lock stayed held for its full 2-hour TTL while the sweep is scheduled hourly -- one malformed list silently blocked custom list updates for every library, every sweep. Two fixes: * update_custom_list_entries now catches QueryParseException alongside RequestError. Both mean "this list's query is malformed", but they are raised from different layers: RequestError is an OpenSearch 400, while QueryParseException comes from our own JSONQuery parser before any request is sent. Catching only the latter let the former abort the sweep. This restores the behavior of the CustomListUpdateEntriesScript this pipeline replaced, which logged and skipped a bad list. * finalize_custom_list_entries_sweep is now registered as the chord body's error callback as well as its body, so the sweep lock is released even when a per-list task fails. Infrastructure errors (database down, OpenSearch transport failure) are still deliberately allowed to propagate and alarm -- they just no longer strand the lock too. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Claude finished @dbernstein's task in 8m 12s —— View job PR Review: Keep one bad custom list from wedging the entries sweep (PP-4506)
SummaryThis is a solid, well-scoped fix and the tests are genuinely convincing. Both changes are correct: I looked specifically for a remaining hole where a bad row could still abort the sweep: |
| @@ -350,18 +364,23 @@ def update_custom_list_entries( | |||
| f"Custom list {custom_list_id} not found; it may have been deleted. " | |||
There was a problem hiding this comment.
Repopulate Can Commit Empty Lists
When a list is in REPOPULATE, _setup_first_invocation deletes its existing entries before populate_query_pages parses and runs the query. With this new catch, a QueryParseException after that delete is logged and swallowed, so the transaction can commit with the list emptied and still stuck in REPOPULATE instead of preserving the last good entries or rolling back.
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #3550 +/- ##
=======================================
Coverage 93.46% 93.46%
=======================================
Files 512 512
Lines 46614 46614
Branches 6352 6352
=======================================
Hits 43570 43570
Misses 1968 1968
Partials 1076 1076 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
Description
Two fixes to
update_custom_list_entries, both aimed at the same failure: a single custom list with a malformedauto_update_querycurrently takes down the entire custom-list entries sweep.1. Catch
QueryParseExceptionalongsideRequestError.The per-list task already intends to log-and-skip a list whose query is malformed, so that one bad list doesn't abort the sweep chord. But it only caught
RequestError(an OpenSearch 400). A query can also be rejected by our ownJSONQueryparser, before any request reaches OpenSearch — that raisesQueryParseException, which sailed straight past the handler and failed the task.This restores the behavior of the
CustomListUpdateEntriesScriptthis pipeline replaced, which wrapped each list in a bareexcept Exception: self.log.exception(...).2. Release the sweep lock when a per-list task fails.
update_custom_list_entriesis a chord header, and Celery runs a chord's body only if every header task succeeded. The body isfinalize_custom_list_entries_sweep, which is what releases the sweep-level Redis lock — so any header failure left that lock held for its full 2-hour TTL. The sweep is beat-scheduled hourly, so the next one or two ticks would skip with "another sweep is already in progress," and then the same thing would happen again.finalizeis now registered as the chord body's error callback as well as its body. Exactly one of the two runs, so the lock is released exactly once. Infrastructure errors (database down, OpenSearch transport failure) still propagate and alarm as before — they just no longer strand the lock on their way out.Motivation and Context
Seen in production (PP-4787). A list's stored query contained a
publishedvalue of2025>01>01— someone typed2025.01.01with the shift key held down. Every sweep, that list raised:which failed the task, skipped the chord body, and left the sweep lock held. The net effect was that no custom list updated for any library, on a permanent, self-renewing cycle — from one bad row.
A follow-up PR (stacked on this one) adds validation of
auto_update_queryat save time in the admin controller, so a query that can't be parsed can't be stored in the first place.How Has This Been Tested?
tox -e py312-docker -- tests/manager/celery/tasks/test_custom_lists.py(24 passed).test_malformed_query_logged_and_swallowedis now parametrized over bothRequestErrorandQueryParseException.test_finalize_registered_as_chord_error_callbackasserts the error callback is wired up and carries the samelock_valueas the body.test_failing_per_list_task_still_releases_sweep_lockis end-to-end: it runs a real sweep whose per-list task raisesSQLAlchemyError, then polls until the sweep lock comes back. I confirmed this test is not vacuous by reverting theon_errorline and watching it fail with "Sweep lock was not released within 10.0 seconds."Checklist
🤖 Generated with Claude Code